import { Injectable, Component, provide } from '@angular/core';
import { Http, Response, RequestOptions, RequestOptionsArgs } from '@angular/http';
import { LoadingService } from "./../shared/utils.module";
import { CmModalService, ModalParams } from "./../modal-module/modal.service";

// import { ReplaySubject } from 'rxjs/ReplaySubject';//改用publishReplay
import { Observable } from 'rxjs/Observable';
import { Observer } from 'rxjs/Observer';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/catch';
import {CacheService, CacheStorageAbstract, CacheLocalStorage, CacheMemoryStorage} from 'ng2-cache/ng2-cache';
import { BASEURL, STOCKURL, apiServer } from "shared";

interface FailedTip {
  status: string | number;
  text: string;
}

export interface RequestAppendOptions {
  isShowLoading?: boolean;
  isShowSuccessModal?: boolean;
  isShowFailedModal?: boolean;
  failedModalTemplate?: string;
  successModalTemplate?: string;
  successTips?: string[];
  errorBak?: any;
  cacheAge?: number;        //seconds
}

const defaultAppendOptions: RequestAppendOptions = {
  isShowLoading: false,
  // isShowLoading: true,
  isShowFailedModal: false,
  isShowSuccessModal: false,
  cacheAge: 0
};

const defaultHttpOptions = {
  withCredentials: true
}

@Injectable()
export class CmHttp {
  private notifier: Notification
  private fileCache
  private queue
  private ziper
  const CACHE_AGE = {
    Instant: 60,
    Moment: 300,
    Hour: 3600,
    Day: 86400,
    Week: 604800
  }
  const PRELOAD_K_PERIODS = ['Day1', 'Min30', 'Min5']
  constructor(
    private http: Http,
    private modal: CmModalService,
    private loading: LoadingService
  ) {
    this.fileCache = new CacheService(new CacheLocalStorage())
    this.fileCache.setGlobalPrefix("zytg")
    this.memCache = new CacheService(new CacheMemoryStorage())
    this.memCache.setGlobalPrefix("zytg")
    this.queue = {};
    this.ziper = require('pako');
    
    window.cacher = {
      memCache: this.memCache,
      fileCache: this.fileCache,
      queue: this.queue,
      clearAll: function(clearUserData) {
        window.cacher.memCache && window.cacher.memCache.removeAll()
        if (window.cacher.fileCache) {
          if (clearUserData) {
            window.cacher.fileCache.removeAll()
          } else {
            for(let i=localStorage.length - 1 ; i >=0; i--){
              if (localStorage.key(i).substring(0, 4) == 'zytg') {
                localStorage.removeItem(localStorage.key(i))
              }
            }
          }
        } 
        if (window.cacher.queue) window.cacher.queue = {}
      }
    }
  }

  get(url: string, options?: RequestOptionsArgs, appendOptions?: RequestAppendOptions): Observable<any> {
    let options = Object.assign({}, defaultHttpOptions, options);
    let fullURL = this.getFullURL(url, options)
    //优先获取队列汇总已有同url请求
    let resp = this.queue[fullURL]
    if (resp) {
      console.log("queue reused", fullURL)
      return resp;
    }

    //优先读取瞬时缓存，减少网络请求
    if (appendOptions && appendOptions.cacheAge > 0) {
      let cache = this.memCache.get(fullURL)
      if (cache) {
        return Observable.of(cache);          
      } 
    }

    resp = this.factory(this.http.get(url, options).take(1), appendOptions, fullURL)
    //检查是否需要切换域名重试
    resp = resp.switchMap(data => {
      if (data.isError && data.needReload) {
        return this.factory(this.http.get(apiServer.getNewURL(url), options).take(1), appendOptions, fullURL)
      } else {
        return Observable.of(data)
      }
    })

    //写入应急文件缓存
    resp = resp.do( data => {
      if (!data.isError) this.setFileCache(url, data, {maxAge: this.CACHE_AGE.Day, tag: 'U'})
    }) 
    //写入瞬时内存缓存
    if (appendOptions && appendOptions.cacheAge > 0) {
      resp = resp.do( data => {
        if (!data.isError) this.memCache.set(fullURL, data, {maxAge: appendOptions.cacheAge})
      }) 
    }

    this.queue[fullURL] = resp     //加入请求队列
    return resp;
  }

  post(url: string, body: any, options?: RequestOptionsArgs, appendOptions?: RequestAppendOptions): Observable<any> {
    return this.factory(this.http.post(url, body, options).take(1), appendOptions);
  }

  put(url: string, body: any, options?: RequestOptionsArgs, appendOptions?: RequestAppendOptions): Observable<any> {
    return this.factory(this.http.put(url, body, options).take(1), appendOptions);
  }

  patch(url: string, body: any, options?: RequestOptionsArgs, appendOptions?: RequestAppendOptions): Observable<any> {
    return this.factory(this.http.patch(url, body, options).take(1), appendOptions);
  }

  delete(url: string, options?: RequestOptionsArgs, appendOptions?: RequestAppendOptions): Observable<any> {
    return this.factory(this.http.delete(url, options).take(1), appendOptions);
  }

  private factory(httpObs: Observable<Response>, appendOptions: RequestAppendOptions, fullURL?: string) {
    let options = Object.assign({}, defaultAppendOptions, appendOptions);
    options.isShowLoading ? this.loading.showLoading("http") : null;
    // console.debug("Http Options: ", options);
    let publish =
      httpObs
        .map(res => res.json())
        .map(res => {
          fullURL && delete this.queue[fullURL];      //从请求队列中移除
          if (res && (res.error_no || res.errDesc)) {
            throw new Error(res.error_info || res.errDesc);
          }
          return res;
        })
        .catch((err, caught) => {
          fullURL && delete this.queue[fullURL];      //从请求队列中移除
          console.error("Http Error: ", err);

          //无网络时增加桌面通知 
          if (err.status == 0 && !this.notifier) {    
            try {
              this.notifier = new Notification('网络异常', {
                body: '网络连接异常，服务器连接不成功。请检查网络环境，并在恢复后重试。'
              })
            } catch (err) {
              this.notifier = err
            }        
            let that = this
            setTimeout(() => {
              that.notifier = null
            }, 30000)

            //自动切换api域名
            apiServer.resetServer();
            //websocket重置
          }
          this.loading.hideLoading("http");

          //存在历史应急缓存者，展示缓存数据
          let url = fullURL;
          if (!url) {
            url = (err._body && err._body.currentTarget) ? err._body.currentTarget.__zone_symbol__xhrURL : null;
          }
          //检查是否需要重试加载
          if (err.status == 0 && apiServer.checkReload(url)) {
            return Observable.of(Object.assign({}, err, { isError: true, needReload: true }));          
          }

          if (!appendOptions || !appendOptions.alreadyCached) {
            let cache = this.getFileCache(url);
            if (cache) {
              return Observable.of(cache);          
            }          
          }
          if (appendOptions && appendOptions.errorBak) {
            return Observable.of(appendOptions.errorBak);          
          } else {
            return Observable.of(Object.assign({}, err, { isError: true }));          
          }
        })
        .publishReplay();
    publish.connect();
    publish
      .subscribe(
      res => {
        this.loading.hideLoading("http");
        // console.debug("%c subscribe res: ", 'color:#0F0', res);
        if (res.isError) {
          // 恒生的委托单等查询错误
          // if (res.message) {
          //   sanitizeError(res);
          //   return;
          // }
          if (res.message || res.statusText === "Unauthorized") {
            sanitizeError(res);
            return;
          }
          options.isShowFailedModal ?
            // 暂时不使用动态模板，直接使用Modal模块默认模板；因此isDynamic参数为false,后续指定模板参数失效
            this.modal.showModal(sanitizeError(res), false, "./http-fail.template", module.id).subscribe() : null;
        }
      },
      err => {
        this.loading.hideLoading("http");
        // console.debug("%c subscribe err: ", 'color:#F00', err);
        // 恒生的委托单等查询错误
        if (err.message || err.statusText === "Unauthorized") {
          sanitizeError(err);
          return;
        }
        options.isShowFailedModal ?
          // 暂时不使用动态模板，直接使用Modal模块默认模板；因此isDynamic参数为false,后续指定模板参数失效
          this.modal.showModal(sanitizeError(err), false, "./http-fail.template", module.id).subscribe() : null;
      });
    if (options.isShowSuccessModal) {
      // console.debug("Http Success With Modal: ", options);
      return publish
        .switchMap(resJson => {
          // console.debug("Success Modal: ", resJson);
          return this.modal.showModal(
            sanitizeSuccess(resJson, options.successTips), false, "./http-success.template")
        })
        .map(modalOutcome => modalOutcome["data"]);
    }
    return publish;
  }

  private getFullURL(url: string, options: RequestOptionsArgs) {
    if (!options || !options.search) return url;
    url = (url.indexOf("?") > 0) ? url + options.search.toString() : url + "?" + options.search.toString();
    return url;
  }

  // 按对象属性排序比较函数(顺排)
  private compare(propertyName) {
    return function (object1, object2) {
      let value1 = object1[propertyName]
      let value2 = object2[propertyName]
      if (value2 < value1) {
        return 1
      } else if (value2 > value1) {
        return -1
      } else {
        return 0
      }
    }
  }

  //封装fileCache写入，淘汰应急文件缓存，加压缩
  private setFileCache(key: string, data: any, options: any){
    key = key.substring(key.indexOf(".com/") + 4) //支持多域名兼容
    let raw = JSON.stringify(data)
    raw = this.ziper.gzip(raw, {to: "string"})
    if (!this.fileCache.set(key, raw, options)){
      this.fileCache.removeTag('U')
      return this.fileCache.set(key, raw, options)
    } else {
      return true;
    }
  }

  //封装fileCache读取，解压缩
  private getFileCache(key: string){
    key = key.substring(key.indexOf(".com/") + 4) //支持多域名兼容
    let cache = this.fileCache.get(key)
    if (cache) {
      try {
        let data = this.ziper.ungzip(cache, {to: "string"});
        data = JSON.parse(data)
        if (data && data.isError) data = null
        return data
      } catch (err) {
        console.error("wrong cache", err)
        return null
      }
    } else {
      return cache
    }
  }

  //根据k线周期获取cache age
  private getCacheAge(period: string){
    switch(period) {
      case "Min5":
        return 120
      case "Min15":
      case "Min30":
        return 300
      case "Min60":
        return 480
      case "Day1":
        return 600
      case "Day7":
      case "Day30":
        return 86400
      default:
        return 60
    }
  }

  //分时线获取（支持预加载K线）
  getTrend(symbol:string, points: number, preloadKPeriods?: string[]): Observable<any> {
    let fullURL = `${STOCKURL}/api/v2/sc/charts/${symbol}/trend?period=Min1&lastNPoints=${points}`;
    //优先获取队列汇总已有同url请求
    let resp = this.queue[fullURL]
    if (resp) {
      console.log("queue reused", fullURL)
      return resp;
    }

    //优先读取瞬时缓存，减少网络请求
    //let cache = this.memCache.get(fullURL)
    //if (cache) {
    //  return Observable.of(cache);          
    //} 

    let resp = this.factory(this.http.get(fullURL, defaultHttpOptions).take(1), {}, fullURL)
    //检查是否需要切换域名重试
    resp = resp.switchMap(data => {
      if (data.isError && data.needReload) {
        return this.factory(this.http.get(apiServer.getNewURL(fullURL), defaultHttpOptions).take(1), {}, fullURL)
      } else {
        return Observable.of(data)
      }
    })
    this.queue[fullURL] = resp     //加入请求队列

    //写入应急文件缓存
    resp = resp.do( data => {
      if (!data.isError) this.setFileCache(fullURL, data, {maxAge: this.CACHE_AGE.Day, tag: 'U'})
    }) 
    //写入瞬时内存缓存
    //resp = resp.do( data => {
    //  if (!data.isError && data.data && data.data.length) this.memCache.set(fullURL, data, {maxAge: this.CACHE_AGE.Instant})
    //}) 

    //一日分时的同步做多周期K线预加载
    if (points < 500) {
      preloadKPeriods = preloadKPeriods ? preloadKPeriods : this.PRELOAD_K_PERIODS
      for (let i in preloadKPeriods) {
        let period = preloadKPeriods[i]
        this.getKline(symbol, period, 're', true).subscribe( data => console.log("K preloaded", symbol, period, data.kData.length));
      };
    }

    return resp
  }

  //K线获取（支持前后补齐和缓存）
  getKline(symbol: string, period: string, right: string, isPreload?: boolen): Observable<any> {
    let fullURL = `${STOCKURL}/api/v2/sc/charts/${symbol}/kline?period=${period}&right=${right}`
    let cacheKey = "k_" + fullURL
    //优先获取队列汇总已有同url请求
    let resp = this.queue[cacheKey]
    if (resp) {
      console.log("queue reused", cacheKey)
      return resp;
    }

    //优先读取瞬时缓存，减少网络请求
    let cache = this.memCache.get(cacheKey)
    if (cache) {
      return Observable.of(cache);          
    } 

    //增量加载各个k线的新老数据
    let lastDay = null
    let lastDayData = []
    cache = this.getFileCache(cacheKey)
    //对有增量时间戳的数据，优先获取所有未过期的cache数据
    if (cache && cache.length) {
      //预加载当发现存在缓存（未过期说明较新），不需再要做增量，留待正式请求再更新
      if (isPreload) {
          console.log("preload meet cache", symbol)
          return Observable.of({kData: cache})
      }
      lastDay = cache[cache.length - 1].time.substring(0, 10)
      //最后一天的数据自动忽略，以备重新获取
      for (let i = cache.length - 1; i >= 0; i--) {
        if (cache[i].time < lastDay) {
          lastDayData = cache.splice(i + 1)
          break
        } else if (i == 0) {
          cache = []
        }
      }
      //转换api所需日期格式
      lastDay = lastDay.replace(/-/g, '')
    }

    let summary = this.get(`${STOCKURL}/api/v2/sc/charts/${symbol}/summary`, {}, {cacheAge: this.CACHE_AGE.Instant})
    let url = (lastDay) ? fullURL + `&startTime=${lastDay}` : fullURL
    let resp = this.factory(this.http.get(url).take(1), {alreadyCached: true})

    //检查是否需要切换域名重试
    summary = summary.switchMap(data => {
      if (data.isError && data.needReload) {
        return this.get(`${STOCKURL}/api/v2/sc/charts/${symbol}/summary`, {}, {cacheAge: this.CACHE_AGE.Instant})
      } else {
        return Observable.of(data)
      }
    })
    resp = resp.switchMap(data => {
      if (data.isError && data.needReload) {
        return this.factory(this.http.get(apiServer.getNewURL(url)).take(1), {alreadyCached: true})
      } else {
        return Observable.of(data)
      }
    })

    resp = Observable.zip(summary, resp, Observable.of(cache ? cache : []), (x, y, z) => {
      let result = {
        summary: x.data ? x.data : {},
        kData: (y.isError || !y.data) ? [...z, ...lastDayData] : [...z, ...y.data.sort(this.compare('time'))]
      }
      if (x.isError || y.isError) result.notCache = true
      return result
    })

    resp = resp.switchMap( result => {
        let firstDayStr = result.kData.length 
                          ? (result.kData[0] && (result.kData[0].time.substring(0, 10) > result.summary.issue_date) 
                              ? "&startTime=" + result.kData[0].time 
                              : null) 
                          : "&all=1"
        //分钟周期不要求一定从上市日开始取
        if (firstDayStr && cache && (cache.length > 500) && (period.substring(0, 3) == 'Min')) {
          firstDayStr = null
        }
        return firstDayStr
          ? this.factory(this.http.get(`https://api.integrity.com.cn/v1/stock/kcharts/${symbol}?period=${period}&right=${right}${firstDayStr}&lastNPoints=1000000000`, {}).take(1))
            .map(historyData => {
              if (historyData.isError) {
                result.notCache = true    //拿不到老记录，不做缓存处理，避免跳空
                historyData = []
              }
              let data = {
                summary: result.summary,
                kData: [...historyData.sort(this.compare('time')), ...result.kData],
                notCache: result.notCache
              }
              return data
            })
          : Observable.of(result)
    })
    resp = resp.do( result => {
      if (result) {
        //从请求队列中移除
        delete this.queue[cacheKey];     
        //写入短期文件缓存 
        if (!result.notCache) this.setFileCache(cacheKey, result.kData, {maxAge: this.CACHE_AGE.Week})
        //写入瞬时内存缓存(不同周期，瞬时缓存时间不同)
        if (!result.notCache && result.kData && result.kData.length) this.memCache.set(cacheKey, result, {maxAge: this.getCacheAge(period)})
      }
    }) 

    this.queue[cacheKey] = resp     //加入请求队列
    return resp
  }
}

const sanitizeError = err => {
  console.warn("Modal Error: ", err);

  // ZoneAwareError, 恒生接口
  if (err.message) {
    return Object.assign({}, {
      tips: [err.message]
    });
  }

  // 恒生访问令牌无效或已过期
  if (err.statusText === "Unauthorized") {
    return Object.assign({}, {
      tips: [JSON.parse(err["_body"])["error_description"]]
    });
  }

  // F10 bad request error
  if (err.statusText === "Internal Server Error") {
    return Object.assign({}, {
      tips: [err["url"], JSON.parse(err["_body"])["errCode"], JSON.parse(err["_body"])["errDesc"]]
    });
  }

  let errMsg = err["_data"] ? JSON.parse(err["_data"]) : (err["_body"] ? JSON.parse(err["_body"]) : { errDesc: "后端服务不可用，请稍候再试" });
  // errCode, errDesc
  if (errMsg && errMsg.errDesc) {
    return Object.assign({}, err, {
      tips: [errMsg.errDesc]
    });
  }
  // error, error_description
  if (errMsg && errMsg.error_description) {
    return Object.assign({}, err, {
      tips: [errMsg.error_description]
    });
  }
  return err;
};

const sanitizeSuccess = (res, tips) => {
  // console.debug("Modal Success: ", res, tips);
  return Object.assign({}, res, {
    tips
  });
};
